// Copyright DustedPixels.com 2008. All rights reserved.

package com.dustedpixels.asm.utils;

import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

import com.dustedpixels.asm.MethodInlinerUtils;
import com.dustedpixels.asm.MethodNodes;


/**
 * Method inliner.
 * 
 * @author micapolos@gmail.com (Michal Pociecha-Los)
 */
public final class MethodInliner {

  /**
   * Returns new instance of {@code MethodVisitor}, which would inline given
   * method in all places it's called in visited method and write the result
   * into underlying output method visitor.
   * 
   * @param out inliner output
   * @param inlinedMethod a method to inline, which is assumed to exist on
   *        visitec class
   * @param visitedClassName name of class owning visited method
   * @param visitedMethodDesc descriptor of visited method
   * @return inlining method visitor
   */
  public static MethodVisitor inline(MethodVisitor out, MethodNode inlinedMethod, String visitedClassName, String visitedMethodDesc) {
    return new MethodInlinerImpl(out, visitedClassName, visitedMethodDesc, inlinedMethod);
  }

  /**
   * Returns new instance of {@code ClassAdapter} which will inline code of
   * given method in all places it's called in visited class and write the
   * result into underlying output class visitor.
   * 
   * @param out inliner output
   * @param inlinedMethod a method to inline which is assumed to exist on
   *        visited class
   * @return inlining class visitor
   */
  public static ClassVisitor inline(final ClassVisitor out, final MethodNode inlinedMethod) {
    return new ClassAdapter(out) {
      private String className;

      @Override
      public void visit(int version, int access, String name, String signature, String superName,
          String[] interfaces) {
        this.className = name;
        super.visit(version, access, name, signature, superName, interfaces);
      }

      @Override
      public MethodVisitor visitMethod(int access, String name, String desc, String signature,
          String[] exceptions) {
        MethodVisitor out = super.visitMethod(access, name, desc, signature, exceptions);
        if (out != null) {
          out = MethodInliner.inline(out, inlinedMethod, className, desc);
        }
        return out;
      }
    };
  }

  /**
   * Returns new instance of {@code ClassVisitor} which would inline code of
   * given method in all places it's called on visited class and write the
   * result to underlying output.
   * 
   * @param out inliner output
   * @param inlinedMethodName inlined method name
   * @param inlinedMethodDesc inlined method descriptor
   * @return inlining class visitor
   */
  public static ClassVisitor inline(final ClassVisitor out, final String inlinedMethodName, final String inlinedMethodDesc) {
    final ClassNode clazz = new ClassNode();
    return new ClassAdapter(clazz) {
      @Override
      public void visitEnd() {
        super.visitEnd();
        MethodNode inlinedMethod = MethodNodes.find(clazz, inlinedMethodName, inlinedMethodDesc);
        if (inlinedMethod == null) {
          throw new IllegalArgumentException("No such method");
        }
        clazz.accept(MethodInliner.inline(out, inlinedMethod));
      }
    };
  }
  
  /**
   * Implementation of method inliner.
   */
  private final static class MethodInlinerImpl extends MethodAdapter {
    private final MethodVisitor out;
    private final String className;
    private final MethodNode inlinedMethod;

    public MethodInlinerImpl(
        MethodVisitor out,
        String className, 
        String methodDesc, 
        MethodNode inlinedMethod) {
      
      super(new MethodNode(0, "foo", methodDesc, null, new String[0]));
      this.out = out;
      this.className = className;
      this.inlinedMethod = inlinedMethod;
    }
  
    @Override
    public void visitEnd() {
      super.visitEnd();
      
      MethodNode method = (MethodNode) this.mv;
      method = MethodInlinerUtils.inlineMethod(className, method, inlinedMethod);
      method.instructions.resetLabels();
      method.accept(out);
    }
  }
}
